package bg.btanev.eightyfour.domain.profile.feed;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;

import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import bg.btanev.eightyfour.domain.Tag;
import bg.btanev.eightyfour.domain.exception.ProfileUpdateException;
import bg.btanev.eightyfour.domain.geo.GeoLocation;
import bg.btanev.eightyfour.domain.profile.ProfileEventsUpdateStrategy;
import bg.btanev.eightyfour.domain.profile.feed.processor.GeoRssProcessor;
import bg.btanev.eightyfour.domain.profile.feed.processor.MRssProcessor;
import bg.btanev.eightyfour.domain.profile.feed.processor.MRssProcessor.FeedMediaGroup;

import com.sun.syndication.feed.synd.SyndCategory;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.fetcher.FetcherEvent;
import com.sun.syndication.fetcher.FetcherException;
import com.sun.syndication.fetcher.FetcherListener;
import com.sun.syndication.fetcher.impl.FeedFetcherCache;
import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache;
import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher;
import com.sun.syndication.io.FeedException;

/**
 *
 * @author B.Tanev
 *
 */
public final class FeedProfileEventUpdateStrategy implements ProfileEventsUpdateStrategy<FeedProfile, FeedProfileEvent> {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(FeedProfileEventUpdateStrategy.class);

	private static final Object initializationLocker = new Object();
	private static FeedProfileEventUpdateStrategy rssProfileUpdateStrategy;

	private FeedProfileEventUpdateStrategy() {
	}

	public static FeedProfileEventUpdateStrategy getInstance() {
		if (rssProfileUpdateStrategy == null) {
			synchronized (initializationLocker) {
				if (rssProfileUpdateStrategy == null) {
					rssProfileUpdateStrategy = new FeedProfileEventUpdateStrategy();
				}
			}
		}

		return rssProfileUpdateStrategy;
	}

	@Override
	public void mergeProfileEvents(final SortedSet<FeedProfileEvent> currentProfileEvents,
			final Collection<FeedProfileEvent> profileEvents) {
		logger.debug("mergeProfileEvents(SortedSet<FeedProfileEvent>, Collection<FeedProfileEvent>) - start"); //$NON-NLS-1$

		Validate.notNull(currentProfileEvents);
		Validate.notNull(profileEvents);

		if (profileEvents.size() == 0) {
			logger.debug("mergeProfileEvents(SortedSet<FeedProfileEvent>, Collection<FeedProfileEvent>) - end"); //$NON-NLS-1$
			return;
		}

		Set<String> identityCollection = new HashSet<String>();
		for (FeedProfileEvent currFeedPe : currentProfileEvents) {
			String guid = currFeedPe.getGuid();
			identityCollection.add(guid);
		}

		for (FeedProfileEvent feedPe : profileEvents) {
			String guid = feedPe.getGuid();
			if (!(identityCollection.contains(guid))) {
				currentProfileEvents.add(feedPe);
			}
		}

		logger.debug("mergeProfileEvents(SortedSet<FeedProfileEvent>, Collection<FeedProfileEvent>) - end"); //$NON-NLS-1$
	}

	@Override
	public List<FeedProfileEvent> retrieveOnlineProfileEvents(final FeedProfile profile) throws ProfileUpdateException {
		logger.debug("retrieveOnlineProfileEvents(FeedProfile) - start"); //$NON-NLS-1$

		Validate.notNull(profile);

		final URL feedUrl;
		try {
			feedUrl = new URL(profile.getFeedUrl());
		} catch (MalformedURLException e) {
			logger.error("retrieveOnlineProfileEvents(FeedProfile)", e); //$NON-NLS-1$

			throw new ProfileUpdateException("Invalid feed profile url: " + profile.getFeedUrl(), e);
		}

		List<FeedProfileEvent> newProfileEvents = null;
		try {
			newProfileEvents = processFeeds(feedUrl, profile);
		} catch (IllegalArgumentException e) {
			throw new ProfileUpdateException("Cannot validate feed url: " + feedUrl, e);
		} catch (IOException e) {
			throw new ProfileUpdateException("TCP error while communicationg with: " + feedUrl, e);
		} catch (FeedException e) {
			throw new ProfileUpdateException("Cannot parse feeds (invalid feed): " + feedUrl, e);
		} catch (FetcherException e) {
			throw new ProfileUpdateException("HTTP error while communicationg with: " + feedUrl, e);
		}

		logger.debug("retrieveOnlineProfileEvents(FeedProfile) - end"); //$NON-NLS-1$
		return newProfileEvents;
	}

	@SuppressWarnings("unchecked")
	private List<FeedProfileEvent> processFeeds(final URL feedUrl, final FeedProfile profile)
			throws IllegalArgumentException, IOException, FeedException, FetcherException {

		logger.debug("processFeeds(URL, FeedProfile) - start"); //$NON-NLS-1$

		assert (feedUrl != null);
		assert (profile != null);

		FeedFetcherCache fetcherCache = HashMapFeedInfoCache.getInstance();
		HttpURLFeedFetcher feedFetcher = new HttpURLFeedFetcher(fetcherCache);

		FeedFetcherListener feedFetcherListener = new FeedFetcherListener();
		feedFetcher.addFetcherEventListener(feedFetcherListener);

		final SyndFeed feed = feedFetcher.retrieveFeed(feedUrl);

		feedFetcher.removeFetcherEventListener(feedFetcherListener);
		if (!(feedFetcherListener.isNewDataAvailable())) {
			logger.debug("processFeeds(URL, FeedProfile) - no data to update - end"); //$NON-NLS-1$
			return null;
		}

		final List<FeedProfileEvent> newProfileEvents = new ArrayList<FeedProfileEvent>(feed.getEntries().size());
		final Iterator<SyndEntry> i = feed.getEntries().iterator();

		Set<FeedMediaGroup> globalMediaGroups;
		Set<FeedMediaContent> globalMediaContent;

		Set<FeedMediaContent> feedMediaContent;

		while (i.hasNext()) {
			SyndEntry entry = i.next();

			// processing tags (from category elements)
			Set<Tag> tags = processTags(entry);

			// processing MRSS specific tags (media:group; media:content)
			globalMediaGroups = new HashSet<FeedMediaGroup>();
			globalMediaContent = new HashSet<FeedMediaContent>();
			processMediaItems(entry, globalMediaGroups, globalMediaContent);

			if (globalMediaGroups.isEmpty()) {
				globalMediaGroups = null;
			}

			if (globalMediaContent.isEmpty()) {
				globalMediaContent = null;
			}

			// extracting tags (from category elements) from MRSS groups
			Set<Tag> mediaTags = extractMediaTags(globalMediaGroups);
			if (mediaTags != null && mediaTags.size() > 0) {
				if (tags == null) {
					tags = new HashSet<Tag>(mediaTags.size());
				}

				tags.addAll(mediaTags);
			}

			feedMediaContent = new HashSet<FeedMediaContent>();

			if (globalMediaGroups != null) {
				for (FeedMediaGroup mediaGroup : globalMediaGroups) {
					feedMediaContent.addAll(mediaGroup.getMediaContentItems());
				}
			}

			if (globalMediaContent != null) {
				feedMediaContent.addAll(globalMediaContent);
			}

			if (feedMediaContent.isEmpty()) {
				feedMediaContent = null;
			}

			GeoLocation geoLocation = processGeoLocation(entry);

			String description = entry.getDescription() != null ? entry.getDescription().getValue() : null;
			if (description == null) {
				List contents = entry.getContents();
				if (contents != null && contents.size() > 0) {
					Object contentObj = contents.get(0);
					if (contentObj instanceof SyndContent) {
						description = ((SyndContent) contentObj).getValue();
					}
				}
			}
			String guid = entry.getUri();

			Date date = entry.getUpdatedDate();
			if (date == null) {
				date = entry.getPublishedDate();
			}

			if (guid != null && date != null) {
				newProfileEvents.add(new FeedProfileEvent(date, profile, entry.getTitle(), description, guid, entry
						.getLink(), entry.getAuthor(), tags, feedMediaContent, geoLocation));
			} else {
				logger.warn("processFeeds(URL, FeedProfile) - No GUID or date data available for current feed"); //$NON-NLS-1$
			}
		}

		logger.debug("processFeeds(URL, FeedProfile) - end"); //$NON-NLS-1$
		return newProfileEvents;
	}

	private Set<Tag> extractMediaTags(final Collection<FeedMediaGroup> mediaGroups) {
		Set<Tag> mediaTags = null;

		if (mediaGroups != null && mediaGroups.size() > 0) {
			mediaTags = new HashSet<Tag>();
			for (FeedMediaGroup mediaGroup : mediaGroups) {
				Set<Tag> mediaItemTags = mediaGroup.getTags();
				if (mediaItemTags != null && mediaItemTags.size() > 0) {
					mediaTags.addAll(mediaItemTags);
				}
			}
		}

		return mediaTags;
	}

	private GeoLocation processGeoLocation(final SyndEntry entry) {
		assert (entry != null);

		GeoLocation geoLocation = null;

		final GeoRssProcessor geoRssProcessor = new GeoRssProcessor(entry);
		if (geoRssProcessor.hasRssModule()) {
			Collection<GeoLocation> geoLocations = geoRssProcessor.processModule();
			if (geoLocations != null && geoLocations.size() > 0) {
				geoLocation = geoLocations.iterator().next();
			}
		}

		return geoLocation;
	}

	private void processMediaItems(final SyndEntry entry, final Collection<FeedMediaGroup> globalMediaGroups,
			final Collection<FeedMediaContent> globalMediaContent) {

		assert (entry != null);
		assert (globalMediaGroups != null);
		assert (globalMediaContent != null);

		final MRssProcessor mrssProcessor = new MRssProcessor(entry);
		if (mrssProcessor.hasRssModule()) {
			mrssProcessor.processModule();

			final Collection<FeedMediaGroup> mediaGroups = mrssProcessor.getGlobalMediaGroups();
			final Collection<FeedMediaContent> mediaContent = mrssProcessor.getGlobalMediaContent();

			if (mediaGroups != null && mediaGroups.size() > 0) {
				globalMediaGroups.addAll(mediaGroups);
			}
			if (mediaContent != null && mediaContent.size() > 0) {
				globalMediaContent.addAll(mediaContent);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private Set<Tag> processTags(final SyndEntry entry) {
		assert (entry != null);

		final List<SyndCategory> categories = entry.getCategories();
		int tagsCount = categories.size();

		Set<Tag> tags = null;

		if (categories != null && tagsCount > 0) {
			tags = new HashSet<Tag>(tagsCount);
			for (SyndCategory category : categories) {
				if (category.getName() != null && category.getName().trim().length() > Tag.MIN_TAG_LENGTH) {
					tags.add(Tag.createTag(category.getName()));
				}
			}
		}

		return tags;
	}

	private class FeedFetcherListener implements FetcherListener {
		private boolean newDataAvailable = true;

		@Override
		public void fetcherEvent(FetcherEvent event) {
			if (FetcherEvent.EVENT_TYPE_FEED_UNCHANGED.equals(event.getEventType())) {
				newDataAvailable = false;
			}
		}

		public boolean isNewDataAvailable() {
			return newDataAvailable;
		}
	}
}
